
前面我們已經使用 Express 建立 Server,也知道可以透過 app.get() 建立 API,不過實際開發一個功能時,通常不會只有「取得資料」這件事。
以筆記系統為例,使用者除了查看筆記之外,還會需要新增筆記、修改內容,甚至刪除不需要的筆記,而這些常見的資料操作,就可以整理成 CRUD。
這一篇我們會以 Notes 筆記功能為例,一次建立一組完整的 CRUD API,並認識 CRUD 與 HTTP Method 的關係、RESTful API 的基本設計,以及 Express 中很常使用的 req.params、req.body 和 express.json()。
這次會完成以下 API:
GET /notes
GET /notes/:id
POST /notes
PATCH /notes/:id
DELETE /notes/:id
目前還不會使用資料庫,而是先用 JavaScript 陣列暫時存放資料,讓我們把注意力放在 CRUD 和 API 的操作流程上。
CRUD 是四種常見資料操作的縮寫,分別是 Create、Read、Update 和 Delete,也就是建立、讀取、更新與刪除資料。
| CRUD | 全名 | 用途 |
|---|---|---|
| C | Create | 建立資料 |
| R | Read | 讀取資料 |
| U | Update | 更新資料 |
| D | Delete | 刪除資料 |
以筆記系統來說,新增一篇筆記就是 Create,查看筆記是 Read,修改筆記內容是 Update,而刪除筆記則是 Delete。雖然不同網站提供的功能很多,但只要涉及資料操作,經常都能從 CRUD 的角度理解。
CRUD 描述的是「想對資料做什麼」,而當前端透過 HTTP 向後端發送 Request 時,通常會搭配不同的 HTTP Method 表達這次操作的目的。
常見的對應方式如下:
| CRUD | HTTP Method | 用途 |
|---|---|---|
| Create | POST | 建立資料 |
| Read | GET | 取得資料 |
| Update | PATCH / PUT | 修改資料 |
| Delete | DELETE | 刪除資料 |
例如 Notes API 可以設計成:
GET /notes
GET /notes/:id
POST /notes
PATCH /notes/:id
DELETE /notes/:id
雖然這幾支 API 都圍繞著 /notes,但因為搭配了不同的 HTTP Method,所以代表的操作也不同。GET /notes 是取得筆記,POST /notes 是建立筆記,而 DELETE /notes/:id 則是刪除某一篇筆記。
RESTful API 是常見的 API 設計方式之一,其中一個重要概念,是讓 URL 表達「資源」,再透過 HTTP Method 表達「要對資源進行什麼操作」。
例如目前操作的資源是筆記,因此 URL 可以使用:
/notes
而不是另外設計成:
/getNotes
/createNote
/updateNote
/deleteNote
因為取得、建立、修改和刪除這些動作,已經可以交給 HTTP Method 表達。
例如:
GET /notes
POST /notes
兩者使用相同的 /notes,但前者表示取得 Notes,後者表示建立新的 Note。
如果要操作特定的一筆資料,可以把資料的 ID 放進 URL,例如:
GET /notes/1
PATCH /notes/1
DELETE /notes/1
在 Express 中,我們可以把這個 ID 寫成動態參數:
/notes/:id
其中 :id 代表這個位置不是固定文字,而是由 Client 傳入的實際值。
正式的後端專案通常會把資料存放在資料庫中,不過目前我們還沒有進入資料庫,因此先使用 JavaScript 陣列模擬資料。
先建立基本的 Express Server:
const express = require("express");
const app = express();
app.use(express.json());
app.listen(3000, function () {
console.log("Server running at http://localhost:3000");
});
接著建立暫時使用的 Notes:
let notes = [
{
id: 1,
title: "學習 Express",
content: "今天開始學習 Express CRUD"
},
{
id: 2,
title: "學習 RESTful API",
content: "理解 HTTP Method 和 API 的關係"
}
];
目前可以先把這個 notes 陣列想像成一個非常簡單的資料庫,接下來的新增、讀取、修改和刪除都會直接操作這個陣列。
不過這些資料只存在程式執行期間,只要 Server 重新啟動,陣列就會重新建立,剛剛新增或修改的內容也會消失。這也是之後為什麼需要資料庫的原因之一。
首先從最簡單的 Read 開始。如果 Client 想取得目前所有筆記,可以建立 GET /notes。
app.get("/notes", function (req, res) {
res.status(200).json({
data: notes
});
});
當 Client 發送下面這個 Request:
GET /notes
Server 就會回傳目前所有 Notes:
{
"data": [
{
"id": 1,
"title": "學習 Express",
"content": "今天開始學習 Express CRUD"
},
{
"id": 2,
"title": "學習 RESTful API",
"content": "理解 HTTP Method 和 API 的關係"
}
]
}
這裡使用 res.status(200) 設定 HTTP Status Code。200 OK 表示這次 Request 已經成功處理,再透過 json() 將資料以 JSON 格式回傳給 Client。
除了取得所有筆記之外,實際網站通常也會需要取得其中一篇筆記。例如 Client 發送:
GET /notes/1
就代表想取得 ID 為 1 的 Note。
在 Express 中,可以把這種會改變的 URL 片段設計成 Route Parameter,例如:
app.get("/notes/:id", function (req, res) {
const id = Number(req.params.id);
const note = notes.find(function (note) {
return note.id === id;
});
if (!note) {
return res.status(404).json({
message: "Note not found"
});
}
res.status(200).json({
data: note
});
});
這裡的 :id 就是 Route Parameter,可以把它理解成 URL 中的動態參數。當 Client 發送:
GET /notes/2
Express 會把 2 這個值放進 req.params。
req.params 是一個物件,專門用來存放 Route 中的動態參數。例如可以先印出:
console.log(req.params);
會得到:
{
id: "2"
}
因此可以透過:
req.params.id
取得 "2"。
需要注意的是,從 URL 取得的參數預設會是字串,但目前 notes 裡的 id 是 Number,所以這裡先使用 Number() 轉型:
const id = Number(req.params.id);
接著再使用 find(),從 notes 陣列中尋找相同 ID 的資料:
const note = notes.find(function (note) {
return note.id === id;
});
如果找到資料,就回傳該篇 Note;如果找不到,find() 會回傳 undefined,因此可以透過 if (!note) 判斷,並回傳 404 Not Found:
if (!note) {
return res.status(404).json({
message: "Note not found"
});
}
這樣一來,GET /notes/:id 就可以根據 URL 中不同的 ID,取得對應的 Note。
接下來實作 Create。如果 Client 想建立新的 Note,可以使用 POST /notes,並把新的筆記內容放在 Request Body 中傳給 Server。
例如 Client 可以傳送:
{
"title": "學習 Node.js",
"content": "今天練習 CRUD"
}
不過這裡有一個問題:Client 傳給 Server 的 JSON,並不是可以直接操作的 JavaScript 物件,而是放在 HTTP Request Body 裡傳送過來的資料。因此 Server 收到之後,需要先把這段 JSON 解析成 JavaScript 可以操作的資料。
在 Express 中,可以使用內建的 Middleware express.json() 來處理這件事:
app.use(express.json());
express.json() 會讀取 JSON 格式的 Request Body,解析完成後,再把結果放到 req.body 裡,讓後面的 Route 可以直接使用。
整個流程可以理解成:
Client 傳送 JSON
↓
JSON 放進 HTTP Request Body
↓
express.json() 解析 Request Body
↓
解析結果放進 req.body
↓
Route 使用 req.body
因為 express.json() 必須先處理 Request,所以通常會寫在建立 app 之後、所有需要讀取 JSON Body 的 Route 之前:
const express = require("express");
const app = express();
app.use(express.json());
app.post("/notes", function (req, res) {
console.log(req.body);
});
app.listen(3000, function () {
console.log("Server running at http://localhost:3000");
});
Express 會按照程式碼的順序處理 Request,所以當 POST /notes 的 Request 進來時,會先經過:
app.use(express.json());
等 Request Body 被解析完成之後,才會進到後面的 POST /notes Route。這時就可以透過 req.body 取得 Client 傳進來的資料。
例如 Client 傳入:
{
"title": "學習 Node.js",
"content": "今天練習 CRUD"
}
解析完成後,req.body 就會是:
{
title: "學習 Node.js",
content: "今天練習 CRUD"
}
因此就可以透過:
req.body.title
req.body.content
分別取得標題與內容。
接著建立 POST /notes:
app.post("/notes", function (req, res) {
const newNote = {
id: notes.length + 1,
title: req.body.title,
content: req.body.content
};
notes.push(newNote);
res.status(201).json({
data: newNote
});
});
這裡先從 req.body 取得 Client 傳入的資料,再建立新的 newNote:
const newNote = {
id: notes.length + 1,
title: req.body.title,
content: req.body.content
};
接著使用 push() 把新的 Note 加入 notes 陣列:
notes.push(newNote);
最後回傳:
res.status(201).json({
data: newNote
});
這裡使用的是 201 Created。和一般成功時常看到的 200 OK 不同,201 更明確地表示這次 Request 成功建立了一個新的資源,因此很適合用在 POST 新增資料的情境。
這次 POST /notes 的流程可以整理成:
POST /notes
↓
Client 在 Request Body 傳入 JSON
↓
express.json() 解析 JSON
↓
解析結果放進 req.body
↓
透過 req.body 取得資料
↓
建立 newNote
↓
notes.push(newNote)
↓
回傳 201 Created
這裡也可以順便和前面的 req.params 做區分:req.params 用來取得 URL 中的動態參數,而 req.body 則用來取得 Request Body 中的資料。
接著實作 Update。假設現在要修改 ID 為 1 的筆記,可以使用:
PATCH /notes/1
並在 Request Body 中只傳入需要修改的欄位:
{
"title": "重新學習 Express"
}
Route 可以寫成:
app.patch("/notes/:id", function (req, res) {
const id = Number(req.params.id);
const note = notes.find(function (note) {
return note.id === id;
});
if (!note) {
return res.status(404).json({
message: "Note not found"
});
}
if (req.body.title !== undefined) {
note.title = req.body.title;
}
if (req.body.content !== undefined) {
note.content = req.body.content;
}
res.status(200).json({
data: note
});
});
這裡會先透過 req.params.id 找出要修改哪一篇 Note,再透過 req.body 取得新的內容。
假設原本資料是:
{
id: 1,
title: "學習 Express",
content: "今天開始學習 Express CRUD"
}
Client 只傳入:
{
"title": "Express CRUD 練習"
}
更新後就會變成:
{
"id": 1,
"title": "Express CRUD 練習",
"content": "今天開始學習 Express CRUD"
}
因為這次沒有傳入 content,所以原本的內容會保留下來。
更新資料時,常看到 PATCH 和 PUT 兩種 HTTP Method。兩者都可以用來修改資料,但概念上有些不同。
PATCH 通常用來表示部分更新,例如只修改 title:
{
"title": "新的標題"
}
而 PUT 通常表示用新的資料完整取代原本的資源。
在實際專案中,不同團隊可能會有自己的 API 規範,不過這次 Notes API 只需要修改部分欄位,因此使用 PATCH。
最後來實作 Delete。如果 Client 想刪除 ID 為 2 的 Note,可以發送:
DELETE /notes/2
可以先使用 findIndex() 找出資料所在的位置,再透過 splice() 把它從陣列中刪除。
app.delete("/notes/:id", function (req, res) {
const id = Number(req.params.id);
const noteIndex = notes.findIndex(function (note) {
return note.id === id;
});
if (noteIndex === -1) {
return res.status(404).json({
message: "Note not found"
});
}
notes.splice(noteIndex, 1);
res.status(200).json({
message: "Note deleted successfully"
});
});
findIndex() 找到資料時會回傳它在陣列中的位置,如果找不到則會回傳 -1,所以我們可以利用這個結果判斷資料是否存在。
找到資料之後:
notes.splice(noteIndex, 1);
代表從 noteIndex 這個位置開始,刪除一筆資料。
把前面的內容整理在一起,完整的 Express CRUD API 如下:
const express = require("express");
const app = express();
app.use(express.json());
let notes = [
{
id: 1,
title: "學習 Express",
content: "今天開始學習 Express CRUD"
},
{
id: 2,
title: "學習 RESTful API",
content: "理解 HTTP Method 和 API 的關係"
}
];
app.get("/notes", function (req, res) {
res.status(200).json({
data: notes
});
});
app.get("/notes/:id", function (req, res) {
const id = Number(req.params.id);
const note = notes.find(function (note) {
return note.id === id;
});
if (!note) {
return res.status(404).json({
message: "Note not found"
});
}
res.status(200).json({
data: note
});
});
app.post("/notes", function (req, res) {
const newNote = {
id: notes.length + 1,
title: req.body.title,
content: req.body.content
};
notes.push(newNote);
res.status(201).json({
data: newNote
});
});
app.patch("/notes/:id", function (req, res) {
const id = Number(req.params.id);
const note = notes.find(function (note) {
return note.id === id;
});
if (!note) {
return res.status(404).json({
message: "Note not found"
});
}
if (req.body.title !== undefined) {
note.title = req.body.title;
}
if (req.body.content !== undefined) {
note.content = req.body.content;
}
res.status(200).json({
data: note
});
});
app.delete("/notes/:id", function (req, res) {
const id = Number(req.params.id);
const noteIndex = notes.findIndex(function (note) {
return note.id === id;
});
if (noteIndex === -1) {
return res.status(404).json({
message: "Note not found"
});
}
notes.splice(noteIndex, 1);
res.status(200).json({
message: "Note deleted successfully"
});
});
app.listen(3000, function () {
console.log("Server running at http://localhost:3000");
});
到這裡,我們就完成了第一組完整的 CRUD API。
這次實作主要使用了三個常見的 HTTP Status Code:
| Status Code | 名稱 | 使用時機 |
|---|---|---|
| 200 | OK | Request 成功 |
| 201 | Created | 成功建立新的資源 |
| 404 | Not Found | 找不到指定資源 |
例如 GET /notes 成功取得資料時,可以回傳 200 OK;POST /notes 成功建立新的 Note 時,可以回傳 201 Created;而 GET /notes/999 找不到對應資料時,就可以回傳 404 Not Found。
之後還會遇到更多 Status Code,例如 400 Bad Request、401 Unauthorized、403 Forbidden 和 500 Internal Server Error,目前先掌握這三個最基本的即可。
API 寫完之後,可以使用 Postman 實際把 CRUD 流程走過一次。
Method 選擇 GET,URL 輸入:
http://localhost:3000/notes
送出後應該會看到目前所有 Notes。
Method 選擇 GET,URL 輸入:
http://localhost:3000/notes/1
送出後應該會看到 ID 為 1 的 Note。
也可以測試不存在的 ID:
http://localhost:3000/notes/999
這時應該會收到 404 Not Found。
Method 選擇 POST,URL 輸入:
http://localhost:3000/notes
接著在 Body 選擇 raw,格式選擇 JSON,再輸入:
{
"title": "我的第三篇筆記",
"content": "今天完成第一組 CRUD API"
}
送出後應該會收到 201 Created,並看到剛剛建立的新 Note。
Method 選擇 PATCH,URL 輸入:
http://localhost:3000/notes/1
Body 輸入:
{
"title": "修改後的筆記標題"
}
送出後,可以再使用 GET /notes/1 確認資料是否真的被修改。
Method 選擇 DELETE,URL 輸入:
http://localhost:3000/notes/2
成功刪除後,再執行:
GET /notes
就可以確認 ID 為 2 的 Note 是否已經從資料中消失。
如果把剛才的操作按照順序跑一次,其實就是一筆資料最基本的生命週期:
POST /notes
建立資料
GET /notes
查看資料
PATCH /notes/:id
修改資料
GET /notes/:id
確認修改結果
DELETE /notes/:id
刪除資料
這就是 CRUD 最核心的概念。
之後不管遇到文章、會員、商品、留言或訂單等功能,都很常看到類似的 API 結構。例如文章可能會有 GET /articles、POST /articles、PATCH /articles/:id 和 DELETE /articles/:id,雖然操作的資源不同,但背後的設計邏輯其實非常接近。
這一篇我們完成了第一組完整的 Notes CRUD API,包含取得全部 Notes、取得單一 Note、新增 Note、修改 Note 和刪除 Note。
目前完成的 API 如下:
GET /notes
GET /notes/:id
POST /notes
PATCH /notes/:id
DELETE /notes/:id
過程中也認識了 req.params、req.body 和 express.json()。req.params 用來取得 URL 中的動態參數,req.body 用來取得 Client 放在 Request Body 中的資料,而 express.json() 則負責解析 JSON 格式的 Request Body。
目前 Notes 還只是存放在 JavaScript 陣列中,所以 Server 一旦重新啟動,資料就會消失。不過在進入資料庫之前,先把 CRUD、HTTP Method 與 RESTful API 之間的關係理解清楚,之後把資料來源換成 PostgreSQL 或其他資料庫時,API 的基本設計邏輯仍然會延續下去。